﻿using Fluid;
using Fluid.Ast;
using Fluid.Parser;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using System.Web;
using ZYing.LiquidThemeEngine.Ast;
using ZYing.LiquidThemeEngine.Config.Schemas;
using ZYing.LiquidThemeEngine.Config.Values;
using ZYing.LiquidThemeEngine.Exceptions;
using ZYing.LiquidThemeEngine.Locales;
using ZYing.LiquidThemeEngine.Objects;

namespace ZYing.LiquidThemeEngine
{
    public class LiquidThemeTemplateContext : TemplateContext
    {
        static LiquidThemeTemplateContext()
        {
            ScopeAccessor = CreateScopeAccessor();
        }
        private static Func<TemplateContext, Scope> ScopeAccessor { get; }

        private static Func<TemplateContext, Scope> CreateScopeAccessor()
        {
            var type = typeof(TemplateContext);
            var property = type.GetProperty("LocalScope", BindingFlags.Instance | BindingFlags.NonPublic);
            if (property == null) throw new NotSupportedException();
            return (Func<TemplateContext, Scope>)property.GetMethod.CreateDelegate(typeof(Func<TemplateContext, Scope>));
        }
        public string Theme { get; }
        public string Layout { get; set; }
        public string Body { get; set; }
        public LiquidThemeEngineOptions LiquidThemeEngineOptions { get; }
        public bool TryGetBody(out string body)
        {
            body = this.Body;
            return !string.IsNullOrWhiteSpace(Body);
        }


        public LiquidThemeTemplateContext(LiquidThemeEngineOptions liquidThemeEngineOptions, string theme, object model = null, bool allowModelMembers = true)
            : base(liquidThemeEngineOptions.TemplateOptions)
        {
            LiquidThemeEngineOptions = liquidThemeEngineOptions;
            Options = liquidThemeEngineOptions.TemplateOptions;
            Theme = theme;
            Model = model;
            AllowModelMembers = allowModelMembers;
        }

        public LiquidThemeTemplateContext SetTemplate(String template)
        {
            SetValue(Constants.ObjectNames.Template, Template.Get(template));
            return this;
        }
        public async Task<IFluidTemplate> GetTemplateAsync(string path)
        {
            if (string.IsNullOrWhiteSpace(path)) throw new ArgumentNullException(nameof(path));
            var key = Constants.AmbientNames.GetTemplateKey(path);
            if (!AmbientValues.TryGetValue(key, out var value))
            {
                var fileContent = await GetContentAsync(path);
                var parser = LiquidThemeEngineOptions.Parser;
                if (!parser.TryParse(fileContent, out var template, out var error)) throw new ParseFailedException(Theme, path, error);
                AmbientValues[key] = value = template;
            }
            return (IFluidTemplate)value;
        }
        public string GetFileVersion(string path)
        {
            if (String.IsNullOrWhiteSpace(path)) return String.Empty;
            var key = Constants.AmbientNames.GetFileVersionKey(path);
            if (!AmbientValues.TryGetValue(key, out var value))
            {
                var file = this.LiquidThemeEngineOptions.FileProvider.GetFileInfo(this.Theme, path);
                if (!file.Exists)
                {
                    value = string.Empty;
                }
                else
                {
                    value = file.LastModified.ToUnixTimeSeconds().ToString();
                }
                AmbientValues[key] = value;
            }
            return (string)value;
        }

        public Scope GetLocalScope()
        {
            return ScopeAccessor(this);
        }

        public async ValueTask<LocaleDictionary> GetLocaleDictionaryAsync(CultureInfo culture)
        {
            var key = Constants.AmbientNames.GetLocaleDictionaryKey(culture);
            if (!AmbientValues.TryGetValue(key, out var obj))
            {
                AmbientValues[key] = obj = await this.CreateLocaleDictionaryAsync(culture);
            }
            return (LocaleDictionary)obj;
        }
        private async Task<LocaleDictionary> CreateLocaleDictionaryAsync(CultureInfo culture)
        {
            var dir = this.LiquidThemeEngineOptions.FileProvider.GetDirectoryContents(this.Theme, Constants.Directories.Locales);
            if (!dir.Exists)
            {
                return LocaleDictionary.Empty;
            }
            var dic = new LocaleDictionary();
            var search = new[] { "*.default.json", $"{culture.TwoLetterISOLanguageName}.json", $"{culture.Name}.json" };
            var loadFiles = new List<string>();

            foreach (var item in search)
            {
                foreach (var file in dir)
                {
                    if (file.IsDirectory) continue;
                    if (!item.Like(item)) continue;
                    var fileContent = await Helper.ReadAsStringAsync(file);
                    var doc = JsonDocument.Parse(fileContent);
                    var filePath = Helper.CombinePath(Constants.Directories.Locales, file.Name);
                    dic.MergeJsonIntoDictionary(Theme, filePath, doc.RootElement, string.Empty);
                    //var fullPath = Path.Combine(dir, file.Name);
                    if (!loadFiles.Contains(filePath)) loadFiles.Add(filePath);
                }
            }

            //foreach (var file in loadFiles) e.AddExpirationToken(FileProvider.Watch(file));
            return dic;
        }

        public bool TryGetLayout(out string layout)
        {
            layout = this.Layout;
            return !string.IsNullOrWhiteSpace(layout);
        }

        public async Task<String> GetContentAsync(string path)
        {
            if (string.IsNullOrWhiteSpace(path)) throw new ArgumentNullException(nameof(path));
            var key = Constants.AmbientNames.GetContentKey(path);
            if (!AmbientValues.TryGetValue(key, out var value))
            {
                var file = this.LiquidThemeEngineOptions.FileProvider.GetFileInfo(Theme, path);
                if (!file.Exists) throw new ThemeFileNotFoundException(this.Theme, path);
                var fileContent = await Helper.ReadAsStringAsync(file);
                AmbientValues[key] = value = fileContent;
            }
            return (string)value;
        }

        public async Task<ThemeSectionSchema> GetSectionSchema(string section)
        {
            if (string.IsNullOrWhiteSpace(section)) throw new ArgumentNullException(nameof(section));
            section = Helper.NormalTemplateName(section);
            var key = Constants.AmbientNames.GetSectionSchemaKey(section);
            if (!AmbientValues.TryGetValue(key, out var schema))
            {
                var path = Helper.CombinePath(Constants.Directories.Sections, Helper.NormalTemplateFile(section));
                var template = (FluidTemplate)await GetTemplateAsync(path);
                SchemaStatement find = null;
                foreach (var item in template.Statements)
                {
                    if (item is SchemaStatement schemaStatement)
                    {
                        find = schemaStatement;
                        break;
                    }
                }
                if (find == null || string.IsNullOrWhiteSpace(find.Text.ToString()))
                {
                    schema = AmbientValues[key] = new ThemeSectionSchema { Name = new FixedLocalizableText(section), };
                }
                else
                {
                    schema = AmbientValues[key] = ThemeSectionSchema.Parse(find.Text.ToString());
                }
            }
            return (ThemeSectionSchema)schema;
        }

        public async Task<ThemeSetting> GetThemeSettingAsync()
        {
            var key = Constants.AmbientNames.Setting;
            if (!AmbientValues.TryGetValue(key, out var setting))
            {
                var path = Helper.CombinePath(Constants.Directories.Config, Constants.Config.SettingsData);
                var json = await this.GetContentAsync(path);
                AmbientValues[key] = setting = ThemeSetting.Parse(json);
            }
            return (ThemeSetting)setting;
        }
        public void SetSlotContent(string name, IReadOnlyList<Statement> statements)
        {
            var key = Constants.AmbientNames.GetSlotContentKey(name);
            AmbientValues[key] = statements;
        }
        public bool TryGetSoltContent(string name, out IReadOnlyList<Statement> statements)
        {
            var key = Constants.AmbientNames.GetSlotContentKey(name);
            if (AmbientValues.TryGetValue(key, out var obj) && obj is IReadOnlyList<Statement> value)
            {
                statements = value;
                return true;
            }
            statements = null;
            return false;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="writer"></param>
        /// <param name="settings"></param>
        /// <param name="id"></param>
        /// <param name="isDynamic">是否为动态调用。非动态调用时，section 必须和 type 一致，否则会输出异常。</param>
        public async Task RenderSectionAsync(TextWriter writer, ThemeSetting settings, string id, bool isDynamic)
        {
            var sections = settings.Sections;
            if (sections == null || !sections.TryGetValue(id, out var setting))
            {
                return;
            }
            if (id != setting.Type && !isDynamic)
            {
                await writer.WriteAsync($"section type '{setting.Type}' and id '{id}' must be the same when a section is used inside a 'section' liquid tag.");
                return;
            }
            var path = Helper.CombinePath(Constants.Directories.Sections, Helper.NormalTemplateFile(setting.Type));
            var template = await GetTemplateAsync(path);
            var schema = await GetSectionSchema(setting.Type);

            var tag = "div";
            var tagId = $"liquid-section-{id}";
            var cls = "liquid-section";
            if (!string.IsNullOrWhiteSpace(schema.Tag))
            {
                tag = schema.Tag;
            }

            if (!string.IsNullOrWhiteSpace(schema.Class))
            {
                cls += " " + schema.Class;
            }
            var section = new Section
            {
                Id = id,
                Settings = setting.Settings,
            };
            if (setting.Blocks != null && setting.BlockOrder != null)
            {
                section.Blocks = new List<Block>();
                foreach (var xid in setting.BlockOrder)
                {
                    if (setting.Blocks.TryGetValue(xid, out var block))
                    {
                        section.Blocks.Add(new Block
                        {
                            Id = xid,
                            Type = block.Type,
                            Settings = block.Settings
                        });
                    }
                }
            }
            await writer.WriteLineAsync();
            await writer.WriteAsync($"<{tag} id=\"{HttpUtility.HtmlAttributeEncode(tagId)}\" class=\"{HttpUtility.HtmlAttributeEncode(cls)}\">");

            this.IncrementSteps();
            this.EnterChildScope();
            this.SetValue(Constants.ObjectNames.Section, section);
            await template.RenderAsync(writer, this.LiquidThemeEngineOptions.TextEncoder, this);
            this.ReleaseScope();

            await writer.WriteAsync($"</{tag}>");
        }
    }
}
